11.1A: Implement a Minimalist Content Provider

Contents:

A content provider is a component that securely manages access to a shared repository of data. It provides a consistent interface for applications to access the shared data. Applications do not access the provider directly but use a content resolver object that provides an interface to and manages the connection with the content provider.

Content providers are useful because:

  • Apps cannot share data in Android—except through content providers.
  • Content providers allow multiple apps to securely access, use, and modify a single data source. Examples: Contacts, game scores, spell-checking dictionary.
  • You can specify levels of access control (permissions) for your content provider.
  • You can store data independently from the app. Having a content provider allows you to change how the data is stored without needing to change the user interface. For example, you can build a prototype using mock data, then use an SQL database for the real app. You could even store some of your data in the cloud and some data locally, and the user interface would remain the same to your application.
  • This architecture separates data from the user interface so development teams can work independently on the client-facing application and server-side components of your app. For larger, complex apps it is very common that the user interface and the data services are developed by different teams. They can even be separate apps. It is not even required that an app with a content provider has a user interface.
  • You can use CursorLoader and other classes that expect to interact with a content provider.
    Note: If your app does not share data with other apps, then your app does not require a content provider. However, because the content provider cleanly separates the implementation of your backend from the user interface, it can also be useful for architecting more complex applications.

The following diagram summarizes the parts of the content provider architecture. Content Provider architecture

Data: The app that creates the content provider owns the data and specifies what permissions other apps have to work with the data.

The data is often stored in a SQLite database, but this is not mandatory. Typically, the data is made available to the content provider as tables, similar to database tables, where each row represents one entry, and each column represents an attribute for that entry. For example, each row in a contact database contains one entry and that entry may have columns for email addresses and phone numbers.

ContentProvider: The content provider provides a public and secure interface to the data, so that other apps can access the data with the appropriate permissions.

ContentResolver: Used by the Activity to query the content provider. The content resolver returns data as a Cursor object which can then be used, for example, by an adapter, to display the data.

Contract class (not shown): The contract is a public class that exposes important information about the content provider to other apps. This usually includes the URIs to access the data, important constants, and the structure of the data that will be returned.

Apps send requests to the content provider using content Uniform Resource Identifiers or URIs. A content URI for content providers has this general form:

scheme://authority/path-to-data/dataset-name

  • scheme (for content URI, this is always content://)
  • authority (represents the domain, and for content providers customarily ends in .provider)
  • path (this represents the path to the data)
  • ID (uniquely identifies the data set to search; such as a file name or table name)

The following URI could be used to request all the entries in the "words" table:

content://com.android.example.wordcontentprovider.provider/words

Designing URI schemes is a topic in and of itself and is not covered in this course.

Content Resolver: The ContentResolver object provides query(), insert(), update(), and delete() methods for accessing data from a content provider and manages all interaction with the content provider for you. In most situations, you can just use the default content resolver provided by the Android system.

In this practical, you will build a basic content provider from scratch. You will create and process mock data so that you can focus on understanding content provider architecture. Likewise, the user interface to display the data is minimal. In the next practical, you will add a content provider to the WordList app, using this minimalist app as your template.

What you should already KNOW

For this practical you should understand how to:

  • Create, build and run interactive apps in Android Studio.
  • Display data in a RecyclerView using an adapter.
  • Abstract and encapsulate data with data models.
  • Create, manage, and interact with a SQLite database using a SQLiteOpenHelper.

What you will LEARN

You will learn:

  • The architecture and anatomy of a content provider.
  • What you need to do to build a minimal content provider that you can use as a template for creating other content providers.

What you will DO

  • You will build a stand-alone app to learn the mechanics of building a content provider.

App Overview

  • This app generates mock data and stores it in a linked list called "words".
  • The app requests data through a content resolver and displays it. The UI consists of one activity with a TextView and two Buttons. The "List all words" button displays all the words, and the "List first word" button displays the first word in the text view.
  • The content provider abstracts and manages the interaction between the data source and the user interface.
  • The Contract defines URIs and public constants.

MinimalistContentProvider app screen.

Note: Minimum SDK Version is API15: Android 4.0.3 IceCreamSandwich and target SDK is the current version of Android (version 23 as of the writing of this book).

Task 1. Create the MinimalistContentProvider project

1.1. Create a project within the given constraints

Create an app with one activity that shows one text view and two buttons. One button shows the first word in our data (the list), and the other button will list all words. Both buttons call onClickDisplayEntries() when they are clicked. For now, this method will use a switch statement to just display a statement that a particular button was clicked. Use the table below as a guideline for setting up your project.

App name

MinimalistContentProvider

One Activity

Empty Activity template

Name: MainActivity

private static final String TAG = MainActivity.class.getSimpleName();

public void onClickDisplayEntries (View view){Log.d (TAG, "Yay, I was clicked!");}

TextView

@+id/textview

android:text="response"

Button

@+id/button_display_all

android:text="List all words"

android:onClick="onClickDisplayEntries"

Button

@+id/button_display_first

android:text="List first word"

android:onClick="onClickDisplayEntries"

1.2. Complete the basic setup

Complete the basic setup of the user interface:

  1. In the MainActivity, create a member variable for the text view and initialize it in onCreate().
  2. In onClickDisplayEntries(), use a switch statement to check which button was pressed. Use the view id to distinguish the buttons. Print a log statement for each case.
  3. In onClickDisplayEntries(), at the end append some text to the textview.
  4. As always, extract the string resources.
  5. Run the app.

Your MainActivity should be similar to this solution.

Solution:

package android.example.com.minimalistcontentprovider;

[... imports]

public class MainActivity extends AppCompatActivity {

   private static final String TAG = MainActivity.class.getSimpleName();

   TextView mTextView;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       mTextView = (TextView) findViewById(R.id.textview);
   }

   public void onClickDisplayEntries(View view) {
       Log.d (TAG, "Yay, I was clicked!");

       switch (view.getId()) {
           case R.id.button_display_all:
               Log.d (TAG, "Yay, " + R.id.button_display_all + " was clicked!");
               break;
           case R.id.button_display_first:
               Log.d (TAG, "Yay, " + R.id.button_display_first + " was clicked!");
               break;
           default:
               Log.d (TAG, "Error. This should never happen.");
       }
       mTextView.append("Thus we go! \n");
   }
}

Task 2. Create a Contract class, a URI, and mock data

The contract contains information about the data that apps need to build queries.

  • Contract is a public class and includes important information for other apps that want to connect to this content provider and access your data.
  • The URI shows how to build URIs to access the data. The URI scheme behaves as an API to access the data. It's very similar to designing REST calls for CRUD. Other applications will use these Content URIs.

2.1. Create the Contract class

  1. Create a new public Java class Contract with the following signature. It must be final.
    public final class Contract {}
    
  2. To prevent someone from accidentally instantiating the Contract class, give it an empty private constructor.
    private Contract() {}
    

2.2. Create the URI

A content URI for content providers has this general form:

scheme://authority/path/id
  • scheme is always content:// for content URIs.
  • authority represents the domain, and for content providers customarily ends in .provider
  • path is the path to the data
  • id uniquely identifies the data set to search

The following URI could be used to request all the entries in the "words" table:

content://com.android.example.wordcontentprovider.provider/words

The URI for accessing the content provider is defined in the Contract so that it is available to any app that wants to query this content provider. Customarily, this is done by defining constants for AUTHORITY, CONTENT_PATH, and CONTENT_URI.

  1. In the Contract class, create a constant for AUTHORITY. To make Authority unique, use the package name extended with "provider." public static final String AUTHORITY = "com.android.example.minimalistcontentprovider.provider";
  2. Create a constant for the CONTENT_PATH. The content path identifies the data. You should use something descriptive, for example, the name of a table or file, or the kind of data, such as "words".
    public static final String CONTENT_PATH = "words";
  3. Create a constant for the CONTENT_URI. This is a content:// style URI that points to one set of data. If you have multiple "data containers" in the backend, you would create a content URI for each.

    Uri is a helper class for building and manipulating URIs. Since it is a never changing string for all instances of the Contract class, you can initialize it statically. public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + CONTENT_PATH);

  4. Create a convenience constant for ALL_ITEMS. This is the dataset name you will use when retrieving all the words. The value is -2 because that's the first lowest value not returned by a method call. static final int ALL_ITEMS = -2;
  5. Create a convenience constant for WORD_ID. This is the id you will use when retrieving a single word. static final String WORD_ID = "id";

2.3. Add the MIME Type

Content providers provide content, and you need to specify what type of content they provide. Apps need to know the structure and format of the returned data, so that they can properly handle it.

MIME types are of the form type/subtype, such as text/html for HTML pages. For your content provider, you need to define a vendor-specific MIME type for the kind of data your content provider returns. The type of vendor-specific Android MIME types is always:

  • vnd.android.cursor.item for one data item (a record)
  • vnd.android.cursor.dir for s set of data (multiple records).

The subtype can be anything, but it is a good practice to make it informative. For example:

  • vnd—a vendor MIME type
  • com.example—the domain
  • provider—it's for a content provider
  • words—the name of the table

Read Implementing ContentProvider MIME types for details.

  1. Declare the MIME time for one data item.
    static final String SINGLE_RECORD_MIME_TYPE = "vnd.android.cursor.item/vnd.com.example.provider.words";
    
  2. Declare the MIME type for multiple records.
    static final String MULTIPLE_RECORD_MIME_TYPE = "vnd.android.cursor.dir/vnd.com.example.provider.words";
    

2.4. Create the mock data

The content provider always presents the results as a Cursor in a table format that resembles a SQL database. This is independent of how the data is actually stored. This app uses a string array of words.

In strings.xml, add a short list of words:

<string-array name="words">
   <item>Android</item>
   <item>Activity</item>
   <item>ContentProvider</item>
   <item>ContentResolver</item>
</string-array>

Task 3. Implement the MiniContentProvider class

3.1. Create the MiniContentProvider class

  1. Create a Java class MiniContentProvider extending ContentProvider. (For this practical, to not use the Create Class > Other > Content Provider menu option.)
  2. Implement the methods (Code > Implement methods).
  3. Add a log tag.
  4. Add a member variable for the mock data.
    public String[] mData;
    
  5. In onCreate(), initialize mData from the array of words, and return true.
    @Override
    public boolean onCreate() {
       Context context = getContext();
       mData = context.getResources().getStringArray(R.array.words);
       return true;
    }
    
  6. Add an appropriate logging message to the insert, delete, and update methods. You will not implement these methods for this practical.
    Log.e(TAG, "Not implemented: update uri: " + uri.toString());
    

3.2. Publish the content provider by adding it to the Android manifest

In order to access the content provider, your app and other apps need to know that it exists. Add a declaration for the content provider to the Android manifest inside a <provider> tag.

The declaration contains the name of the content provider and the authorities (its unique identifier).

  1. In the AndroidManifest, inside the application tag, after the activity closing tag, add:
    <provider
        android:name=".MiniContentProvider"
    android:authorities="com.android.example.minimalistcontentprovider.provider" />
    
  2. Run your code to make sure it compiles.

3.3. Set up URI matching

A ContentProvider needs to respond to data requests from apps using a number of different URIs. To take appropriate action depending on a particular request URI, the content provider needs to analyze the URI to see if it matches. UriMatcher is a helper class that you can use for processing the accepted URI schemes for a given content provider.

Basic steps to use UriMatcher:

  • Create an instance of UriMatcher.
  • Add each URI that your content provider recognizes to the UriMatcher.
  • Assign each URI a numeric constant. Having a numeric constant for each URI is convenient when you are processing incoming URIs because you can use a switch/case statement on the numeric values to work through the URIs.

Make the following changes in the MiniContentProvider class.

  1. In the MiniContentProvider class, create a private static variable for a new UriMatcher.

    The argument to the constructor specifies the value to return if there is no match. As a best practice, use UriMatcher.NO_MATCH.

    private static UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    
  2. Create your own method for initializing the URI matcher.
    private void initializeUriMatching(){}
    
  3. Call initializeUriMatching in onCreate() of the MiniContentProvider class.
  4. In the initializeUriMatching() method, add the URIs that your content provider accepts to the matcher and assign them an integer code. These are the URIs based on the authority and content paths specified in the contract.

    The # symbol matches a string of numeric characters of any length. In this app, it refers to the index of the word in the string array. In a production app, this could be the id of an entry in a database. Assign this URI a numeric value of 1.

    sUriMatcher.addURI(Contract.AUTHORITY, Contract.CONTENT_PATH + "/#", 1);
    
  5. The second URI is the one you specified in the contract for returning all items. Assign it a numeric value of 0. sUriMatcher.addURI(Contract.AUTHORITY, Contract.CONTENT_PATH, 0);

Note that if your app is more complex and uses more URIs, use named constants for the codes, as shown in the UriMatcher documentation.

Solution:

private void initializeUriMatching(){
   sUriMatcher.addURI(Contract.AUTHORITY, Contract.CONTENT_PATH + "/#", 1);
   sUriMatcher.addURI(Contract.AUTHORITY, Contract.CONTENT_PATH, 0);
}

3.4. Implement the getType() method

The getType() method of the content provider returns the MIME type for each of the specified URIs.

Unless you are doing something special in your code, this method implementation is going to be very similar for any content provider. It does the following:

  1. Match the URI.
  2. Switch on the returned code.
  3. Return the appropriate MIME type.

Learn more in the UriMatcher documentation.

Solution:

public String getType(Uri uri) {
        switch (sUriMatcher.match(uri)) {
            case 0:
                return Contract.MULTIPLE_RECORD_MIME_TYPE;
            case 1:
                return Contract.SINGLE_RECORD_MIME_TYPE;
            default:
                // Alternatively, throw an exception.
                return null;
        }
    }

3.5 Implement the query() method

The purpose of the query() method is to match the URI, convert it to a your internal data access mechanism (for example a SQlite query), execute your internal data access code, and return the result in a Cursor object.

The query() method

The query method has the following signature:

public Cursor query(Uri uri, String[] projection, String selection,String[] selectionArgs, String sortOrder){}

The arguments to this method represent the parts of a SQL query. Even if you are using another kind of data storage mechanism, you must still accept a query in this style and handle the arguments appropriately. (In the next task you will build a query in the MainActivity to see how the arguments are used.) The method returns a Cursor of any kind.

uri

The complete URI. This cannot be null.

projection

Indicates which columns/attributes to access.

selection

Indicates which rows/records of the objects to access.

selectionArgs

The binding parameters to the previous selection argument.

For security reasons, the arguments are processed separately.

sortOrder

Whether to sort, and if so, whether ascending, descending or by .

If this is null, the default sort or no sort is applied.

Analyze the query() method

  1. Identify the following processing steps in the query() method code shown below in the solutions section.

    Query processing always consists of these steps:

    1. Match the URI.

    2. Switch on the returned code.

    3. Process the arguments and build a query appropriate for the backend.

    4. Get the data and (if necessary) drop it into a Cursor.

    5. Return the cursor.

  2. Identify portions of the code that need to be different in a real-world application.

    The query implementation for this basic app takes some shortcuts.

    • Error handling is minimal.
    • Because the app is using mock data, the Cursor can be directly populated.
    • Because the URI scheme is simple, this method is rather short.
  3. Identify at least one design decision that makes it easier to understand and maintain the code.
    • Analyzing the query and executing it to populate a cursor are separated into two methods.
    • The code contains more comments than executable code.
  4. Add the code to your app.

Note: You will get an error for the populateCursor() method, and will address this in the next step.

Annotated Solution Code for the query() method in MiniContentProvider.java

@Nullable
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
   int id = -1;
   switch (sUriMatcher.match(uri)) {
       case 0:
           // Matches URI to get all of the entries.
           id = Contract.ALL_ITEMS;
           // Look at the remaining arguments
           // to see whether there are constraints.
           // In this example, we only support getting
           //a specific entry by id. Not full search.
           // For a real-life app, you need error-catching code;
           // here we assume that the
           // value we need is actually in selectionArgs and valid.
           if (selection != null){
               id = parseInt(selectionArgs[0]);
           }
           break;

       case 1:
           // The URI ends in a numeric value, which represents an id.
           // Parse the URI to extract the value of the last,
           // numeric part of the path,
           // and set the id to that value.
           id = parseInt(uri.getLastPathSegment());
           // With a database, you would then use this value and
           // the path to build a query.
           break;

       case UriMatcher.NO_MATCH:
           // You should do some error handling here.
           Log.d(TAG, "NO MATCH FOR THIS URI IN SCHEME.");
           id = -1;
           break;
       default:
           // You should do some error handling here.
           Log.d(TAG, "INVALID URI - URI NOT RECOGNIZED.");
           id = -1;
   }
   Log.d(TAG, "query: " + id);
   return populateCursor(id);
}

3.6. Implement the populateCursor() method

Once the query() method has identified the URI, it calls your populateCursor() with the last segment of the path, which is the id (index) of the word to retrieve. The populateCursor() method separates the query matching from getting the data and creating the result cursor. This is a good practice as in a real app, the query() method can become very large.

The query method must return a Cursor type, so the populateCursor() method has to create, fill in, and return a cursor.

  • If your data were stored in a SQLite database, executing the query would return a Cursor.
  • If you are not using a data storage method that returns a cursor, such as files or the mock data, you can use a MatrixCursor to hold the data to return. A MatrixCursor is a general purpose cursor into an array of objects that grows as needed. To create a MatrixCursor, you supply it with a string array of column names.

The populateCursor() method does the following:

  1. Receives the id extracted from the URI.
  2. Creates a MatrixCursor to store received data (because the mock data received is not a cursor).
  3. Creates and executes a query. For this app, this gets the string at the index id from the string array. In a more realistic app, this could execute a query to a database.
  4. Adds the result to the cursor.
  5. Returns the cursor.
    private Cursor populateCursor(int id) {
       MatrixCursor cursor = new MatrixCursor(new String[] { Contract.CONTENT_PATH });
       // If there is a valid query, execute it and add the result to the cursor.
       if (id == Contract.ALL_ITEMS) {
           for (int i = 0; i < mData.length; i++) {
               String word = mData[i];
               cursor.addRow(new Object[]{word});
           }
       } else if (id >= 0) {
           // Execute the query to get the requested word.
           String word = mData[id];
           // Add the result to the cursor.
           cursor.addRow(new Object[]{word});
       }
       return cursor;
    }
    

Task 4. Use a ContentResolver to get data

With the content provider in place, the onClickDisplayEntries() method in the MainActivity can be expanded to query and display data to the UI. This requires the following steps:

  1. Create the SQL-style query, depending on which button was pressed.
  2. Use a content resolver to interact with the content provider to execute the query and return a Cursor.
  3. Process the results in the Cursor.

4.1. Get the content resolver

The content resolver interacts with the content provider on your behalf.

The content resolver expects a parsed Content URI along with query parameters that assist in retrieving the data.

You don't have to create your own content resolver. You can use the one provided in your application context by the Android framework by calling getContentResolver().

  1. In MainActivity, remove all code from inside onClickDisplayEntries().
  2. Add this code to onClickDisplayEntries() in MainActivity.
    Cursor cursor = getContentResolver().query(Uri.parse(queryUri), projection, selectionClause, selectionArgs, sortOrder);
    
    Note: the arguments to getContentResolver.query() are identical to the parameters of ContentProvider.query().

Next you must define the arguments to getContentResolver.query().

4.2. Define the query arguments

In order for getContentResolver.query() to work, you need to declare and assign values to all its arguments.

  1. URI: Declare the ContentURI that identifies the content provider and the table. Get the information for the correct URI from the contract.
    String queryUri = Contract.CONTENT_URI.toString();
    
  2. Projection: A string array with the names of the columns to return. Setting this to null returns all columns. When there is only one column, as in the case of this example, setting this explicitly is optional, but can be helpful for documentation purposes. // Only get words. String[] projection = new String[] {Contract.CONTENT_PATH};
  3. selectionClause: Argument clause for the selection criteria, that is, which rows to return. Formatted as an SQL WHERE clause (excluding the"WHERE" keyword). Passing null returns all rows for the given URI. Since this will vary depending on which button was pressed, declare it now and set it later.
    String selectionClause;
    
  4. selectionArgs: Argument values for the selection criteria. If you include ?s in the selection String, they are replaced by values from selectionArgs, in the order that they appear.
    IMPORTANT: It is security best practices to always separate selection and selectionArgs.
    String selectionArgs[];
  5. sortOrder: The order in which to sort the results. Formatted as a SQL ORDER BY clause (excluding the ORDER BY keyword). Usually ASC or DESC; null requests the default sort order, which could be unordered.
    // For this example, accept the order returned by the response.
    String sortOrder = null;
    

4.3. Decide on selection criteria

The selectionClause and selectionArgs values depend on which button was pressed in our UI.

  • To display all the words, set both arguments to null.
  • To get the first word, query for the word with the ID of 0. (This assumes that word IDs start at 0 and are created in order. You know this, because the information is exposed in the contract. For a different content provider, you may not know the ids, and may have to search in a different way.)
  1. Replace the existing switch block with the following code in onClickDisplayEntries, before you get the content resolver.
    switch (view.getId()) {
       case R.id.button_display_all:
           selectionClause = null;
           selectionArgs = null;
           break;
       case R.id.button_display_first:
           selectionClause = Contract.WORD_ID + " = ?";
           selectionArgs = new String[] {"0"};
           break;
       default:
           selectionClause = null;
           selectionArgs = null;
    }
    

4.4. Process the Cursor

After getting the content resolver, you have to process the result from the Cursor.

  • If there is data, display it in the text view.
  • If there is no data, report errors.
  1. Examine the following code and make sure you understand everything.
       if (cursor != null) {
       if (cursor.getCount() > 0) {
           cursor.moveToFirst();
           int columnIndex = cursor.getColumnIndex(projection[0]);
           do {
               String word = cursor.getString(columnIndex);
               mTextView.append(word + "\n");
           } while (cursor.moveToNext());
       } else {
           Log.d(TAG, "onClickDisplayEntries " + "No data returned.");
           mTextView.append("No data returned." + "\n");
       }
       cursor.close();
    } else {
       Log.d(TAG, "onClickDisplayEntries " + "Cursor is null.");
       mTextView.append("Cursor is null." + "\n");
    }
    
  2. Insert this code at the end of onClickDisplayEntry().
  3. Run your app.
  4. Click the buttons to see the retrieved data in the text view.

Solution code

Android Studio project: MinimalistContentProvider

Coding challenges

Note: All coding challenges are optional and are not prerequisites for later lessons.

Implement missing methods

Coding Challenge 1: Implement the insert, delete, and update methods for the MinimalistContentProvider app. Provide the user with a way to insert, delete, and update data.

Hint: If you don't want to build out the user interface, create a button for each action and hardwire the data that is inserted, updated, and deleted. The point of this exercise is to work on the content provider, not the user interface.

Why: You will implement the fully functioning content provider with UI in the next practical, when you will add a content provider to the WordListSQL app.

Add Unit Tests for the content provider

Coding Challenge 2: After you implemented the content provider, there was no way for you to know whether or not the code would work. In this sample, you built out the front-end and by watching it work, assumed the app worked correctly. In a real-life app, this is not sufficient, and you may not even have access to a front-end. The appropriate way for determining that each method acts as expected, write a set of unit tests for MiniContentProvider.

Summary

In this chapter, you learned

  • Content providers are high-level data abstractions that manages access to a shared repository
  • Content providers are primarily intended to be used by apps other than your own.
  • Content providers (server-side) are accessed by Content resolvers (app-side)
  • A Contract is a public class that exposes important information about a content provider.
  • contracts can be useful beyond content providers,
  • A content provider needs to define a set of content URIs so apps can access data through your content provider.
  • The content URI consists of several components: "content://", a unique content authoriity (typically a fully-qualified package name) and the content-path.
  • Use a content resolver to request data from a content provider and display it to the user.
  • If your app does not share data with other apps, then your app does not require a content provider.
  • Content providers must implement the getType() method which returns the MIME type for each content type.
  • Content Providers need to be "published" in the Android manifest using the element within the element
  • Content Providers need to inspect the incoming URI to determine URI pattern matches in order to access any data
  • You must add target URI patterns to your content provider. The UriMatcher class is a helpful class for this purpose.
  • The essence of a content provider is implemented in its query() method.
  • The method signature of the query() method in a content resolver (data requester) matches the method signature of the query() method in a content provider (data source).
  • The query() method returns a database-style cursor object regardless if the data is relational or not.

The related concept documentation is in Android Developer Fundamentals: Concepts.

Learn more

Developer Documentation:

results matching ""

    No results matching ""